﻿namespace Sina.Weibo
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using System.Web;

    /// <summary>
    /// WeiboAuthClientCore class provides the core implementation of authentication/authorization logic
    /// behind the public WeiboAuthClient class
    /// </summary>
    internal class WeiboAuthClientCore
    {
        private readonly WeiboAuthClient publicAuthClient;
        private readonly string clientId;
        private readonly string clientSecret;
        private IEnumerable<string> initScopes;
        private HttpContextBase webContext;
        private WeiboLoginResult loginStatus;
        private TaskCompletionSource<WeiboLoginResult> currentTask;
        private string appRequestState;

        private delegate void CompleteTaskHandler(WeiboLoginResult session);

        /// <summary>
        /// Initializes an new instance of the WeiboAuthClientCore class.
        /// </summary>
        public WeiboAuthClientCore(
            string clientId,
            string clientSecret,
            WeiboAuthClient authClient)
        {
            Debug.Assert(!string.IsNullOrEmpty(clientId));
            Debug.Assert(!string.IsNullOrEmpty(clientSecret));
            Debug.Assert(authClient != null);

            this.clientId = clientId;
            this.clientSecret = clientSecret;
            this.publicAuthClient = authClient;
        }

        /// <summary>
        /// Initializes the WeiboAuthClient instance for the current user. 
        /// If an authorization code is present, it will send a request to the auth server to exchange the token.
        /// If there is an auth session in current context, it will retrieve the current auth session.
        /// Any updated session state will be saved in the auth session cookie.
        /// </summary>
        public Task<WeiboLoginResult> ExchangeAuthCodeAsync(
            string redirectUrl,
            HttpContextBase webContext)
        {
            Debug.Assert(webContext != null);
            Debug.Assert(!string.IsNullOrEmpty(redirectUrl));

            this.ValidateConflictAuthTask();

            this.webContext = webContext;
            var taskSource = new TaskCompletionSource<WeiboLoginResult>();
            this.currentTask = taskSource;

            string authorizationCode;
            WeiboAuthException error;
            this.LoadSession(out authorizationCode, out error);

            // If this page receives an authorization code, then exchange the code
            // with the auth server to get the tokens.
            if (!string.IsNullOrEmpty(authorizationCode))
            {
                // We intentionally move auth actions into a wrapping asynchronous process to work around an issue where
                // invoking Task Async methods will trigger an error to be thrown on an Async Asp.Net page (.aspx) code.
                Task.Factory.StartNew(() =>
                {
                    this.ExchangeCodeForToken(
                        redirectUrl,
                        authorizationCode);
                });
            }
            else if (error != null)
            {
                // We received error from the auth server response.
                if (error.ErrorCode == AuthErrorCodes.AccessDenied)
                {
                    // Access_denied should be treated as NotConnected.
                    WeiboLoginResult result = new WeiboLoginResult(WeiboConnectSessionStatus.NotConnected, null);
                    this.OnAuthTaskCompleted(result);
                }
                else
                {
                    // We received some other error, then throw it.
                    this.OnAuthTaskCompleted(new WeiboLoginResult(error));
                }
            }
            else
            {
                // This is exchange auth code only action, but there is neither code nor error return.
                // The app developer may invoke this at wrong location.
                error = new WeiboAuthException(AuthErrorCodes.ClientError, ErrorText.AuthServerResponseNotAvailable);
                this.OnAuthTaskCompleted(new WeiboLoginResult(error));
            }

            return taskSource.Task;
        }

        /// <summary>
        /// Generates a consent URL that includes a set of provided parameters.
        /// </summary>
        public string GetLoginUrl(string redirectUrl, IEnumerable<string> scopes,
            string state, DisplayType display, bool forceLogin, string language)
        {
            return WeiboAuthUtility.BuildAuthorizeUrl(this.clientId,
                redirectUrl, scopes, state, display, forceLogin, language);
        }

        private void ValidateConflictAuthTask()
        {
            // We don't allow auth tasks to be executed concurrently.
            if (this.currentTask != null)
            {
                throw new InvalidOperationException(ErrorText.ExistingAuthTaskRunning);
            }
        }

        private void LoadSession(out string authCode, out WeiboAuthException error)
        {
            authCode = null;
            error = null;

            if (this.webContext != null)
            {
                // Reads current login status from session cookie.
                HttpContextUtility.ReadAuthCodeRequest(
                    this.webContext, out authCode, out appRequestState, out error);
            }
            else
            {
                this.loginStatus = new WeiboLoginResult(WeiboConnectSessionStatus.Unknown, null);
            }
        }

        private void OnAuthTaskCompleted(
            WeiboLoginResult loginResult)
        {
            if (loginResult != null)
            {
                if (loginResult.Session != null)
                {
                    WeiboAuthException error = this.ValidateSession(loginResult.Session);
                    if (error != null)
                    {
                        loginResult = new WeiboLoginResult(error);
                    }
                    else
                    {
                        // We have a new session, update the WeiboAuthClient.Session.
                        this.loginStatus = loginResult;
                    }
                }
            }
            else
            {
                // We should return the existing status for cases like already initialized or can't refresh ticket.
                loginResult = this.loginStatus;
            }

            this.CompleteAuthTask(loginResult);
        }

        private void CompleteAuthTask(WeiboLoginResult loginResult)
        {
            Debug.Assert(loginResult != null);

            loginResult = ValidateSessionInitScopes(loginResult);

            if (loginResult.Session != null)
            {
                // Only update Session property if there is a new session.
                this.publicAuthClient.Session = loginResult.Session;
            }

            this.publicAuthClient.FirePendingPropertyChangedEvents();

            TaskCompletionSource<WeiboLoginResult> taskSource = this.currentTask;
            if (taskSource != null)
            {
                this.currentTask = null;

                if (loginResult.Error != null)
                {
                    var error = loginResult.Error as WeiboAuthException;
                    if (error == null)
                    {
                        error = new WeiboAuthException(AuthErrorCodes.ClientError, error.Message, loginResult.Error);
                    }

                    error.State = this.appRequestState;
                    taskSource.SetException(loginResult.Error);
                }
                else
                {
                    loginResult.State = this.appRequestState;
                    taskSource.SetResult(loginResult);
                }
            }
        }

        private WeiboLoginResult ValidateSessionInitScopes(WeiboLoginResult loginResult)
        {
            if (loginResult.Session != null && this.initScopes != null)
            {
                if (!WeiboAuthUtility.IsSubsetOfScopeRange(this.initScopes, loginResult.Session.Scopes))
                {
                    loginResult = new WeiboLoginResult(WeiboConnectSessionStatus.NotConnected, null);
                }

                this.initScopes = null;
            }

            return loginResult;
        }

        private void ExchangeCodeForToken(string redirectUrl, string authorizationCode)
        {
            Task<WeiboLoginResult> task = WeiboAuthRequestUtility.ExchangeCodeForTokenAsync(
                this.clientId, this.clientSecret, redirectUrl, authorizationCode);
            task.ContinueWith((tk) =>
            {
                this.OnAuthTaskCompleted(tk.Result);
            });
        }

        /// <summary>
        /// Validate if the user Id from the received session matches the one from the refresh token and current session.
        /// </summary>
        private WeiboAuthException ValidateSession(WeiboConnectSession session)
        {
            Debug.Assert(session != null);

            string currentUserId = null;
            string userId = null;
            WeiboAuthException error = null;
            WeiboConnectSession currentSession = (this.loginStatus == null) ? null : this.loginStatus.Session;

            // Read current session user Id, if avaliable.
            if (currentSession != null)
            {
                currentUserId = currentSession.UserId;
            }

            // Read user Id from the new session received from the auth server.
            userId = session.UserId;

            if (!string.IsNullOrEmpty(currentUserId) &&
                string.Compare(userId, currentUserId, StringComparison.InvariantCultureIgnoreCase) != 0)
            {
                // The user Id should match current session user Id.
                error = new WeiboAuthException(
                    AuthErrorCodes.InvalidRequest,
                    ErrorText.NewSessionDoesNotMatchCurrentUserId);
            }

            return error;
        }
    }
}
